-
Notifications
You must be signed in to change notification settings - Fork 210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add FilterPriority to prioritize some set of options over another #36
base: master
Are you sure you want to change the base?
Conversation
This is an idea that I have been kicking around in my mind for a while. One of the major functionality lacking from The implementation is nice that it only involves adding a new type I'm not happy with the name This pull request has no tests! I will add them if we think these options are a good idea. |
I haven't thought about this in detail yet, so these are just a few ideas:
|
I see two complications with this:
EDIT: The type Comparator interface {
Compare(v1, v2 interface{}, path *Path, dc Comparator) ComparisonResult
} The |
I think that recursive comparison may become a necessary feature at some point, but perhaps I'm wrong about that. I'd envision something like: type EqualFunc func(x, y interface{}, opts ...Option) bool
Comparer(func(x, y T, eq EqualFunc) bool {
return eq(x, y, additionalOptions...)
}) The |
I think this is problematic when you do something like: Comparer(func(x, y []MyStruct, eq EqualFunc) bool {
... // Check that len(x) == len(y)
for i := 0; i < len(x); i++ {
// This is subtly wrong since eq doesn't know that
// a SliceIndex path step should be added.
if !eq(x[i], y[i]) {
return false
}
}
return true
}) It is also problematic when you do: Comparer(func(x, y []MyStruct, eq EqualFunc) bool {
// What if eq already has an EquateApprox option?
// Does mine take precedence?
return eq(x, y, EquateApprox(...))
}) Also, if we had the API suggested, then it is unfortunate that we introduced Comparer(func(x, y T, eq EqualFunc) bool {
x2, y2 := transform(x), transform(y)
// But how does eq know that a transform occurred?
return eq(x2, y2)
})
I can't think of any use-case that this can accomplish that can't be done with the current set of options and |
Actually, the Thus, (ignoring filters to prevent infinite cycles), this would look like: cmp.Transformer(func(src []MyStruct, eq EqualFunc) (dst []MyStruct) {
for i := range src {
// Again, how does eq know that a SliceIndex step occurred here?
if !eq(src[i], MyStruct{}) {
dst = append(dst, src[i])
}
}
return dst
}) My concern about incorrect paths still remains. Unless there is a satisfiable answer to My current solution to #28, was to provide DiscardElements(func(v MyStruct) bool {
// EquateEmpty and proto.Equal may well have been already options passed to the
// original invocation of cmp.Equal, but at least there's no question here what
// options are applicable here.
return cmp.Equal(v, MyStruct{}, EquateEmpty(), cmp.Comparer(proto.Equal))
}) |
Apologies for being too busy (OK, lazy) to read this whole proposal in detail. But from the POV of an interested consumer of this API, it is getting seriously complicated. I'd urge you all to consider three things:
Basically, adding priorities feels like jumping the shark to me. |
Yes. All three examples in the description were driven by real cases. The solution to work around them involved some really ugly set of filters to negate some set while applying another. Thus, example 1 would look like: FilterTree(T1, OT1),
FilterTree(T2, OT2),
FilterTree(T, FilterTree(!T1, FilterTree(!T2, OT)))
FilterTree(!T, O), In reality, this becomes nasty since filters are not standalone values that you can compose nicely together.
Isn't that what
Isn't that what |
I wasn't clear. I meant reconsider the idea that the ordering of options in the list matters. Use the ordering as a priority. |
It's a large benefit that generally the ordering of options do not matter and that I don't need to think about it. In a sense, all If that wasn't what you thought |
Added API: Default() Option FilterPriority(opts ...Option) Option FilterPriority returns a new Option where an option, opts[i], is only evaluated if no fundamental options remain after applying all filters in all prior options, opts[:i]. In order to prevent further options from being evaluated, the Default option can be used to ensure that some fundamental option remains. Suppose you have a value tree T, where T1 and T2 are sub-trees within T. Prior to the addition of FilterPriority, it was impossible to do certain things. Example 1: You could not make the following compose together nicely. * Have a set of options OT1 to affect only values under T1. * Have a set of options OT2 to affect only values under T2. * Have a set of options OT to affect only T, but not values under T1 and T2. * Have a set of options O to affect all other values, but no those in T (and by extension those in T1 and T2). Solution 1: FilterPriority( // Since T1 and T2 do not overlap, they could be placed within the // same priority level by grouping them in an Options group. FilterTree(T1, OT1), FilterTree(T2, OT2), FilterTree(T, OT), O, ) Example 2: You could not make the following compose together nicely. * Have a set of options O apply on all nodes except those in T1 and T2. * Instead, we want the default behavior of cmp on T1 and T2. Solution 2: FilterPriority( // Here we show how to group T1 and T2 together to be on the same // priority level. Options{ FilterTree(T1, Default()), FilterTree(T2, Default()), }, O, ) Example 3: You have this: type MyStruct struct { *pb.MyMessage; ... } * Generally, you want to use Comparer(proto.Equal) to ensure that all proto.Messages within the struct are properly compared. However, this type has an embedded proto (generally a bad idea), which causes the MyStruct to satisfy the proto.Message interface and unintentionally causes Equal to use proto.Equal on MyStruct, which crashes. * How can you have Comparer(proto.Equal) apply to all other proto.Message without applying just to MyStruct? Solution 3: FilterPriority( // Only for MyStruct, use the default behavior of Equal, // which is to recurse into the structure of MyStruct. FilterPath(func(p Path) bool { return p.Last().Type() == reflect.TypeOf(MyStruct{}) }, Default()), // Use proto.Equal for all other cases of proto.Message. Comparer(proto.Equal), )
(I'm not resurrecting this PR yet) Note to self: Think carefully about whether By analogy, if evaluation for if o1, ok := opts.(*priorityFilter); ok {
for _, o2 := range o1.opts {
if o2, ok := o2.(*priorityFilter); ok {
for _, o3 := range o2.opts {
if IsDefault(o3) {
// break or return?
}
...
}
}
...
}
}
... As of 5b174e0, the behavior of |
Since it seems this may be a blocker for #75 I've read over this issue. Looking at the examples, it seems most of this could be implemented already. Example 3 (already mentioned in #36 (comment)) I believe could be implemented this way: FilterPath(func(p Path) bool {
return p.Last().Type() != reflect.TypeOf(MyStruct{})
}, Comparer(proto.Equal)), I believe the other two examples could use the same idea (a negative path filter). This could be made more convenient by exposing more path filters (as suggested in #75), along with a func PathNot(f func(Path) bool) f func(Path) bool {
return func(path Path) bool {
return !f(path)
}
} So example 3 becomes: FilterPath(PathNot(PathType(MyStruct{})), Comparer(proto.Equal)) For examples 1 and 2 you would use some other Path filter function that matches the root of the tree (instead of PathType). |
In issue #75, we discussed how The benefit of |
I thought about that a bit more, and I'm not really seeing the problem. There are two interfaces:
For the majority of cases a simple path filter seems to be appropriate. In the rare case when you want to filter by something more than paths you can create something that returns PathNot and ValueNot can be composed using func Something() Option {
return FilterPath(PathNot(x), ValueNot(y))
} Any reason that won't work? Edit: I believe
Possibly just me, but "not this path" seems easier to understand than a list of filter priorities. "not path" can be understood in isolation, it doesn't depend on other filters and options. To understand FilterPriority you have to first figure out what paths/values would match every previous Option in the list. |
Comparer options cannot be used with other Comparer options or Transformer options. Unfortunatelly, go-cmp currently doesn't provide a nice way to compose several such options (see: google/go-cmp#36). An Ignore option seems to be more suitable semantically in this case and it does not conflict with other options.
EDIT (2017-08-09): Renamed
Continue
asDefault
Added API:
FilterPriority
returns a newOption
where an option,opts[i]
,is only evaluated if no fundamental options remain after applying all filters
in all prior options,
opts[:i]
. In order to prevent further options from being evaluated,the
Default
option can be used to ensure that some fundamental option remains.Suppose you have a value tree T, where T1 and T2 are sub-trees within T.
Prior to the addition of
FilterPriority
, it was impossible to do certain things.Example 1: You could not make the following compose together nicely.
Solution 1:
Example 2: You could not make the following compose together nicely.
Solution 2:
Example 3: You have this:
type MyStruct struct { *pb.MyMessage; ... }
Comparer(proto.Equal)
to ensure that allproto.Messages
within the struct are properly compared.However, this type has an embedded proto (generally a bad idea),
which causes the
MyStruct
to satisfy theproto.Message
interface andunintentionally causes
cmp.Equal
to useproto.Equal
onMyStruct
, which crashes.Comparer(proto.Equal)
apply to all otherproto.Message
without applying just toMyStruct
?Solution 3: